# -*- coding: utf-8 -*-
"""
GENERATOR ATP OTOMATIS  (v2)
Sumber rujukan: Kepkabadan Kemendikdasmen No. 046/H/KR/2025

FORM MASUKAN (hanya 4 hal yang perlu diisi):
  1. Mata Pelajaran
  2. Fase / Kelas        -> E/X, F/XI, atau F/XII
  3. JP per minggu
  4. Minggu efektif      -> ganjil [ ]  genap [ ]

STRUKTUR SEMESTER (otomatis):
  Fase E / Kelas X    = Semester 1 (Ganjil) + Semester 2 (Genap)
  Fase F / Kelas XI   = Semester 3 (Ganjil) + Semester 4 (Genap)
  Fase F / Kelas XII  = Semester 5 (Ganjil) [PKL - tidak digenerate]
                        + Semester 6 (Genap)

PEMBAGIAN ELEMEN:
  Dibagi rata; bila jumlahnya ganjil, kelebihannya diberikan ke
  semester GANJIL karena minggu efektifnya lebih banyak.

Contoh:
  python3 atpgen/atp.py --form
  python3 atpgen/atp.py --list "otomotif"
  python3 atpgen/atp.py --mapel "Seni Tari" --jp 2 --mg 22 --mn 15
  python3 atpgen/atp.py --mapel "Teknik Sepeda Motor" --kelas XI --jp 8 --mg 20 --mn 14
"""
import argparse
import json
import math
import os
import re
import sys

from docx import Document
from docx.shared import Pt, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.section import WD_ORIENT
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml.ns import qn
from docx.oxml import OxmlElement

DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cp_db.json")

# Folder penyimpanan hasil: folder induk dari "atpgen" (lintas sistem operasi).
BASE_OUT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if not os.path.isdir(BASE_OUT):
    BASE_OUT = os.getcwd()
FONT = "Arial"

# ============================================================================
# 1. STRUKTUR SEMESTER RESMI
# ============================================================================

# (fase, kelas) -> daftar semester: (nomor, label, jenis, aktif, catatan)
STRUKTUR = {
    ("E", "X"): [
        (1, "SEMESTER 1 (GANJIL)", "ganjil", True, ""),
        (2, "SEMESTER 2 (GENAP)", "genap", True, ""),
    ],
    ("F", "XI"): [
        (3, "SEMESTER 3 (GANJIL)", "ganjil", True, ""),
        (4, "SEMESTER 4 (GENAP)", "genap", True, ""),
    ],
    ("F", "XII"): [
        (5, "SEMESTER 5 (GANJIL)", "ganjil", False,
         "PRAKTIK KERJA LAPANGAN (PKL) \u2013 TIDAK ADA PEMBELAJARAN DI SEKOLAH"),
        (6, "SEMESTER 6 (GENAP)", "genap", True, ""),
    ],
}

KELAS_FASE = {"X": "E", "XI": "F", "XII": "F"}

# rentang kewajaran minggu efektif (untuk peringatan, bukan pembatas)
WAJAR_GANJIL = (17, 22)
WAJAR_GENAP = (13, 16)


def semester_aktif(fase, kelas):
    return [s for s in STRUKTUR[(fase, kelas)] if s[3]]


# ============================================================================
# 2. BASIS DATA CP
# ============================================================================


def load_db():
    with open(DB_PATH, encoding="utf-8") as f:
        return json.load(f)


def norm(s):
    return re.sub(r"[^a-z0-9 ]", " ", s.lower()).strip()


def cari_mapel(db, query, fase=None):
    q = norm(query)
    qtok = [t for t in q.split() if t]
    hasil = []
    for s in db:
        nama = norm(s["nama_asli"])
        if fase and fase not in s["fase"]:
            continue
        skor = 0
        if nama == q:
            skor = 1000
        elif q and q in nama:
            skor = 500 - abs(len(nama) - len(q))
        else:
            hit = sum(1 for t in qtok if t in nama)
            if hit == len(qtok) and qtok:
                skor = 200 - abs(len(nama) - len(q))
            elif hit:
                skor = hit * 20
        if skor:
            hasil.append((skor, s))
    hasil.sort(key=lambda x: -x[0])
    return [s for _, s in hasil]


# ============================================================================
# 3. LIBRARY 8 DIMENSI PROFIL LULUSAN
# ============================================================================

DPL = [
    ("Keimanan dan Ketakwaan terhadap Tuhan YME", [
        "agama", "iman", "takwa", "ibadah", "akhlak", "spiritual", "religius",
        "etika", "moral", "nilai budaya", "kejujuran", "amanah", "syukur"]),
    ("Kewargaan", [
        "budaya", "tradisi", "bangsa", "indonesia", "masyarakat", "sosial",
        "lingkungan", "global", "kebinekaan", "pancasila", "norma", "hukum",
        "regulasi", "standar", "peraturan", "pelanggan"]),
    ("Penalaran Kritis", [
        "analis", "menganalisis", "evaluasi", "mengevaluasi", "identifikasi",
        "mengidentifikasi", "membandingkan", "memahami", "menjelaskan",
        "menginterpretasi", "menyimpulkan", "memecahkan", "masalah", "diagnos",
        "pemeriksaan", "menilai", "mengkaji", "menentukan", "menghitung",
        "pengukuran", "troubleshoot", "logika", "prosedur", "klasifikasi"]),
    ("Kreativitas", [
        "mencipta", "merancang", "mengembangkan", "inovasi", "kreasi", "kreatif",
        "desain", "gagasan", " ide", "membuat", "karya", "komposisi",
        "modifikasi", "produk", "rekayasa", "solusi", "eksplorasi"]),
    ("Kolaborasi", [
        "kelompok", "bersama", "kerja sama", "kerjasama", "kolabor", "tim",
        "diskusi", "berkelompok", "ansambel", "gotong", "mitra", "industri"]),
    ("Kemandirian", [
        "mandiri", "diri sendiri", "individu", "tanggung jawab", "disiplin",
        "refleksi", "percaya diri", "inisiatif", "wirausaha", "kewirausahaan",
        "profesional", "etos", "kesiapan kerja", "karier", "belajar"]),
    ("Kesehatan", [
        "k3", "k3lh", "keselamatan", "kesehatan", "kebugaran", "tubuh",
        "ergonomi", "apd", "higiene", "sanitasi", "lingkungan kerja",
        "budaya kerja", "kecelakaan", "risiko", "limbah", "gerak"]),
    ("Komunikasi", [
        "komunikasi", "mengomunikasikan", "presentasi", "mempresentasikan",
        "menyajikan", "laporan", "melaporkan", "menyampaikan", "lisan",
        "tulisan", "publikasi", "pertunjukan", "menampilkan", "gambar teknik",
        "simbol", "dokumentasi", "layanan", "negosiasi"]),
]


def pilih_dpl(*teks, **kw):
    minimal = kw.get("minimal", 3)
    maksimal = kw.get("maksimal", 4)
    blob = " ".join(t.lower() for t in teks if t)
    skor = []
    for nama, keys in DPL:
        skor.append((sum(blob.count(k) for k in keys), nama))
    skor.sort(key=lambda x: -x[0])
    dipilih = [n for s, n in skor if s > 0][:maksimal]
    if len(dipilih) < minimal:
        for s, n in skor:
            if n not in dipilih:
                dipilih.append(n)
            if len(dipilih) >= minimal:
                break
    urut = [n for n, _ in DPL]
    dipilih.sort(key=urut.index)
    return "\n".join("\u2022 " + d for d in dipilih)


# ============================================================================
# 4. LIBRARY MEDIA PEMBELAJARAN
# ============================================================================

MEDIA_DIGITAL = ["Komputer/laptop & jaringan internet", "LCD proyektor",
                 "Software/IDE sesuai materi", "Video tutorial",
                 "Platform LMS (Google Classroom)", "Jobsheet digital / LKPD"]

MEDIA_RUMPUN = [
    (["tari", "musik", "teater", "seni", "karawitan", "pedalangan"],
     ["Video pertunjukan/karya seni", "LCD proyektor & sound system",
      "Ruang praktik/aula & cermin", "Properti dan kostum",
      "Gawai untuk dokumentasi", "Canva / CapCut"]),
    (["otomotif", "sepeda motor", "kendaraan", "alat berat", "mesin", "pengelasan",
      "pemesinan", "manufaktur", "ototronik", "body"],
     ["Unit/trainer praktik & engine stand", "Tool set dan alat ukur (SST)",
      "Service manual & wiring diagram", "LCD proyektor & video animasi kerja",
      "Alat Pelindung Diri (APD)", "Jobsheet digital / LKPD"]),
    (["listrik", "elektronika", "instalasi", "mekatronika", "otomasi"],
     ["Trainer kit elektronika/kelistrikan", "Multimeter & alat ukur listrik",
      "Software simulasi (Proteus/EKTS)", "LCD proyektor", "APD kelistrikan",
      "Jobsheet digital / LKPD"]),
    ["rekayasa perangkat lunak", "komputer", "jaringan", "informatika", "koding",
     "kecerdasan artifisial", "animasi", "multimedia", "desain komunikasi visual"],
    (["akuntansi", "bisnis", "pemasaran", "manajemen", "perkantoran", "logistik",
      "ekonomi", "kewirausahaan"],
     ["LCD proyektor & PowerPoint/Canva", "Aplikasi spreadsheet (Excel/Sheets)",
      "Software akuntansi/aplikasi kasir", "Formulir & dokumen transaksi",
      "Studi kasus DUDI", "LKPD digital (Google Form)"]),
    (["kuliner", "tata boga", "perhotelan", "pariwisata", "kecantikan", "spa",
      "busana", "tata rias"],
     ["Ruang praktik/dapur/salon", "Peralatan dan bahan praktik",
      "Video demonstrasi prosedur", "LCD proyektor", "APD & perlengkapan higiene",
      "Jobsheet digital / LKPD"]),
    (["pertanian", "agribisnis", "peternakan", "perikanan", "kehutanan", "tanaman"],
     ["Lahan/kandang/kolam praktik", "Alat dan sarana produksi",
      "Video prosedur budidaya", "LCD proyektor", "APD pertanian",
      "Jobsheet digital / LKPD"]),
    (["kesehatan", "keperawatan", "farmasi", "laboratorium medik", "dental"],
     ["Phantom/manekin peraga", "Alat kesehatan & laboratorium",
      "Video prosedur klinis", "LCD proyektor", "APD standar kesehatan",
      "Jobsheet digital / LKPD"]),
    (["konstruksi", "bangunan", "gambar", "geospasial", "survei", "furnitur", "kayu"],
     ["Alat gambar & meja gambar", "Software CAD/BIM",
      "Peralatan ukur (theodolite/waterpass)", "LCD proyektor", "APD konstruksi",
      "Jobsheet digital / LKPD"]),
    (["bahasa", "inggris", "indonesia", "arab", "jepang", "jerman", "korea",
      "mandarin", "prancis"],
     ["Audio/video autentik (podcast, film pendek)", "LCD proyektor & speaker",
      "Kamus digital & AI language tools", "Teks bacaan cetak/digital",
      "Quizizz / Kahoot", "LKPD digital (Google Form)"]),
    (["pendidikan agama", "pancasila", "sejarah", "sosiologi", "geografi",
      "antropologi", "ilmu pengetahuan sosial"],
     ["LCD proyektor & PowerPoint/Canva", "Video dokumenter & sumber digital",
      "Peta/atlas & infografis", "Artikel dan studi kasus", "Padlet / Jamboard",
      "LKPD digital (Google Form)"]),
    (["matematika", "fisika", "kimia", "biologi", "ilmu pengetahuan alam", "projek ilmu"],
     ["LCD proyektor & PowerPoint", "Alat peraga & KIT laboratorium",
      "Software simulasi (GeoGebra/PhET)", "Lembar kerja & data eksperimen",
      "Kalkulator/spreadsheet", "LKPD digital (Google Form)"]),
    (["jasmani", "olahraga"],
     ["Lapangan/ruang praktik olahraga", "Peralatan olahraga",
      "Video teknik gerak", "Stopwatch & alat ukur kebugaran",
      "Gawai untuk analisis gerak", "Lembar observasi digital"]),
]

MEDIA_DEFAULT = ["LCD proyektor & PowerPoint/Canva", "Video pembelajaran",
                 "Buku teks & modul ajar", "Gawai & jaringan internet",
                 "Platform LMS (Google Classroom)", "LKPD digital (Google Form)"]


def pilih_media(nama_mapel, elemen, jumlah=5):
    blob = (nama_mapel + " " + elemen).lower()
    for entry in MEDIA_RUMPUN:
        if isinstance(entry, list):
            if any(k in blob for k in entry):
                return "\n".join("\u2022 " + m for m in MEDIA_DIGITAL[:jumlah])
            continue
        keys, media = entry
        if any(k in blob for k in keys):
            return "\n".join("\u2022 " + m for m in media[:jumlah])
    return "\n".join("\u2022 " + m for m in MEDIA_DEFAULT[:jumlah])


# ============================================================================
# 5. PENURUNAN ATP, TP, DAN MATERI POKOK
# ============================================================================


def pecah_klausa(cp):
    t = re.sub(r"\s+", " ", cp).strip()
    bagian = re.split(r";|\.\s+(?=[A-Z])", t)
    out = []
    for b in bagian:
        b = b.strip(" .;")
        if not b:
            continue
        if len(b) > 220:
            sub = re.split(r",\s+(?=(?:serta|dan|meliputi|termasuk)\b)|,\s+(?=[a-z]{4,}\s)", b)
            buf = ""
            for s in sub:
                s = s.strip(" ,")
                if not s:
                    continue
                if len(buf) + len(s) < 200:
                    buf = (buf + ", " + s).strip(" ,")
                else:
                    if buf:
                        out.append(buf)
                    buf = s
            if buf:
                out.append(buf)
        else:
            out.append(b)
    return [o for o in out if len(o) > 12]


def kapital(s):
    s = s.strip()
    return s[0].upper() + s[1:] if s else s


def kecil_awal(s):
    s = s.strip()
    return s[0].lower() + s[1:] if s else s


def buat_atp(cp, elemen):
    klausa = pecah_klausa(cp) or [cp]
    langkah = ["%d. %s." % (i + 1, kapital(k)) for i, k in enumerate(klausa[:6])]
    if len(langkah) < 3:
        nm = elemen.split("(")[0].strip().lower()
        for t in ["Mengidentifikasi konsep dasar dan istilah kunci pada elemen %s." % nm,
                  "Mempraktikkan/menerapkan kompetensi sesuai prosedur dan standar yang berlaku.",
                  "Menyajikan hasil kerja serta melakukan refleksi dan tindak lanjut perbaikan."]:
            if len(langkah) >= 4:
                break
            langkah.append("%d. %s" % (len(langkah) + 1, t))
    return "\n".join(langkah)


def buat_tp(cp, elemen):
    klausa = pecah_klausa(cp) or [cp]
    tp = ["%d. Murid mampu %s." % (i + 1, kecil_awal(k)) for i, k in enumerate(klausa[:5])]
    if len(tp) < 2:
        tp.append("%d. Murid mampu menyajikan hasil belajar pada elemen %s secara bertanggung jawab."
                  % (len(tp) + 1, elemen.split("(")[0].strip().lower()))
    return "\n".join(tp)


def _sub_frasa(k):
    """Pecah satu klausa Capaian Pembelajaran menjadi sub-frasa bermakna.

    Pemecahan hanya dilakukan pada penanda perincian yang benar-benar ada di
    dalam teks Capaian Pembelajaran (koma, "serta", "dan", "meliputi"), sehingga
    tidak ada materi baru yang ditambahkan.
    """
    bagian = re.split(r",\s*|\s+serta\s+|\s+meliputi\s+|\s+dan\s+(?=[a-z]{4,})", k)
    out = []
    for b in bagian:
        b = b.strip(" .;,")
        b = re.sub(r"^(serta|dan|meliputi|termasuk)\s+", "", b, flags=re.I)
        if len(b) > 14:
            out.append(b)
    return out


# Tahap kognitif bertingkat (dipakai HANYA bila jumlah rincian di dalam Capaian
# Pembelajaran belum mencukupi jumlah pertemuan). Urutannya mengikuti taksonomi
# dari mengenali -> memahami -> menerapkan -> menganalisis -> mengevaluasi ->
# menyajikan, dan tetap memakai materi dari Capaian Pembelajaran itu sendiri.
TAHAP_TP = [
    "mengidentifikasi bagian, istilah, dan konsep kunci pada",
    "menjelaskan prinsip dan cara kerja pada",
    "menerapkan langkah kerja sesuai prosedur yang berlaku pada",
    "menganalisis permasalahan dan penyebab kesalahan pada",
    "mengevaluasi hasil kerja serta melakukan perbaikan pada",
    "menyajikan hasil kerja dan laporan mengenai",
]


def _inti_frasa(teks, batas=95):
    """Buang kata kerja di depan sehingga tersisa pokok materinya."""
    t = re.sub(r"^(meng|meny|mem|men|mer|me|ber)[a-z\-]+\s+", "", teks.strip(), flags=re.I)
    t = re.sub(r"\s+", " ", t).strip(" .;,")
    if len(t) > batas:
        t = t[:batas].rsplit(" ", 1)[0] + "\u2026"
    return t or teks


KATA_ID = (" dan ", " yang ", " pada ", " serta ", " dengan ", " untuk ",
           " dalam ", " atau ", " sesuai ", " mampu ", " tidak ", " dari ",
           " ke ", " oleh ", " agar ", " secara ", " tentang ", " adalah ")

# Kata berawalan "me-" yang sesungguhnya bukan kata kerja.
BUKAN_KERJA = ("media", "metode", "mesin", "meja", "memori", "mekanik",
               "mekanika", "mekanisme", "meteran", "mesinnya", "berbagai",
               "berita", "berbentuk", "bermacam", "bersih")


def _tanpa_terjemahan(teks):
    """Buang bagian dalam tanda kurung yang berisi terjemahan bahasa Inggris.

    Dokumen CP mata pelajaran bahasa asing menuliskan terjemahan Inggris di
    dalam tanda kurung; bagian itu tidak boleh ikut menjadi Tujuan Pembelajaran.
    Tanda kurung yang isinya bahasa Indonesia (singkatan, contoh, penjelasan)
    tetap dipertahankan. Kurung bersarang \u2014 mis. terjemahan panjang yang di
    dalamnya masih ada "(cetak atau digital)" \u2014 ikut ditangani, termasuk bila
    kurung penutupnya tidak lengkap.
    """
    s = re.sub(r"\s+", " ", teks or "")
    hasil = []
    i = 0
    while i < len(s):
        if s[i] != "(":
            hasil.append(s[i])
            i += 1
            continue
        dalam = 0
        j = i
        while j < len(s):
            if s[j] == "(":
                dalam += 1
            elif s[j] == ")":
                dalam -= 1
                if dalam == 0:
                    break
            j += 1
        if j >= len(s):
            isi, akhir = s[i + 1:], len(s)
        else:
            isi, akhir = s[i + 1:j], j + 1
        # isi kurung yang lebih dalam tidak dipakai untuk menebak bahasanya
        uji = " " + re.sub(r"\([^()]*\)", " ", isi).lower() + " "
        if len(isi) < 25 or any(k in uji for k in KATA_ID):
            hasil.append(s[i:akhir])
        else:
            hasil.append(" ")
        i = akhir
    bersih = re.sub(r"\s{2,}", " ", "".join(hasil))
    bersih = re.sub(r"\s+([.,;:])", r"\1", bersih)
    return re.sub(r"[,;]\s*([.;])", r"\1", bersih).strip(" ,;")


def _ada_kerja(frasa):
    """Benar bila frasa dibuka oleh kata kerja (berawalan me-/ber-)."""
    kata = re.sub(r"[^A-Za-z\- ]", " ", frasa or "").strip().split(" ")
    if not kata or not kata[0]:
        return False
    k = kata[0].lower()
    if k in BUKAN_KERJA:
        return False
    return bool(re.match(r"^(me|ber)[a-z]{4,}$", k))


def _kerja_awal(klausa):
    """Kata kerja pertama pada satu klausa Capaian Pembelajaran."""
    for w in re.sub(r"[^A-Za-z\- ]", " ", klausa or "").split():
        if _ada_kerja(w):
            return w.lower()
    return None


def _rapikan_pecahan(bagian, klausa=""):
    """Pastikan tiap pecahan klausa tetap berupa rumusan yang berkata kerja.

    Pecahan yang tidak berkata kerja \u2014 mis. "tersirat dari berbagai jenis teks"
    \u2014 tidak dijadikan Tujuan Pembelajaran tersendiri, melainkan disatukan kembali
    dengan pecahan sebelumnya supaya rumusannya utuh dan setia pada naskah CP.
    """
    kerja = _kerja_awal(klausa)
    out = []
    for b in bagian:
        b = (b or "").strip(" .;,")
        if not b:
            continue
        if _ada_kerja(b):
            out.append(b)
        elif out:
            out[-1] = out[-1].rstrip(" .;,") + ", " + b
        elif kerja:
            out.append("%s %s" % (kerja, b))
        else:
            out.append(b)
    return out


# Batas jumlah Tujuan Pembelajaran untuk SETIAP Capaian Pembelajaran.
# Jumlah ini tidak bergantung pada jumlah pertemuan: satu pertemuan boleh
# memuat beberapa TP, dan satu TP boleh diuraikan dalam beberapa pertemuan.
MIN_TP = 4
MAKS_TP = 8


def _gabung_butir(rinci, maks):
    """Ratakan jumlah butir menjadi `maks` dengan menggabung butir yang berdekatan.

    Dipakai bila perincian di dalam Capaian Pembelajaran ternyata lebih dari
    batas atas. Penggabungan hanya menyambung teks yang sudah ada, jadi tidak
    ada materi yang hilang maupun materi baru yang ditambahkan.
    """
    n = len(rinci)
    if n <= maks:
        return rinci
    out = []
    i = 0
    for g in range(maks):
        sisa_grup = maks - g
        ambil = max(1, -(-(n - i) // sisa_grup))
        grup = rinci[i:i + ambil]
        i += ambil
        gab = grup[0].strip(" .;,")
        for x in grup[1:]:
            gab = gab.rstrip(" .;,") + ", " + kecil_awal(x.strip(" .;,"))
        out.append(gab)
    return out


SASARAN_TP = 6

# Kata sambung/keterangan yang tidak boleh membuka sebuah Tujuan Pembelajaran.
AWALAN_SAMBUNG = ("dan", "serta", "atau", "meliputi", "termasuk", "yaitu",
                  "antara lain", "baik", "pada", "dalam", "untuk", "sesuai",
                  "dengan", "dari", "secara", "yang", "seperti", "sebagai",
                  "melalui", "berdasarkan", "agar", "sehingga", "maupun")


def _potong_tp(teks, batas=170):
    """Jaga panjang satu butir Tujuan Pembelajaran agar tetap terbaca."""
    x = re.sub(r"\s+", " ", teks or "").strip(" .;,:")
    if len(x) > batas:
        x = x[:batas].rsplit(" ", 1)[0].rstrip(" .;,:") + "\u2026"
    return x


def _pokok_cp(teks):
    """Ambil daftar pokok materi dari naskah Capaian Pembelajaran.

    Pemecahan hanya memakai penanda perincian yang benar-benar tertulis di
    dalam naskah (titik koma, koma, "serta", "dan", "meliputi"), lalu kata
    kerja dan kata sambung di depan dibuang sehingga tersisa pokok materinya.
    Tidak ada materi baru yang ditambahkan.
    """
    sisip = "|".join(AWALAN_SAMBUNG)
    hasil = []
    for k in re.split(r";|\.\s+(?=[A-Z])", _tanpa_terjemahan(teks or "")):
        pecah = re.split(r",\s*|\s+serta\s+|\s+meliputi\s+|\s+termasuk\s+"
                         r"|\s+dan\s+(?=[a-z]{4,})", k)
        for b in pecah:
            b = b.strip(" .;,:")
            if not b:
                continue
            if _ada_kerja(b):
                b = re.sub(r"^[A-Za-z\-]+\s+", "", b)
            for _ in range(3):
                baru = re.sub(r"^(?:" + sisip + r")\s+", "", b, flags=re.I)
                if baru == b:
                    break
                b = baru
            b = b.strip(" .;,:")
            if len(b) >= 8 and len(b.split()) >= 2:
                hasil.append(b)
    return hasil


def _kunci(x):
    return re.sub(r"[^a-z ]", "", (x or "").lower())[:45]


def _tumpang(x, daftar):
    """Benar bila pokok materi `x` sudah terwakili oleh salah satu butir daftar.

    Dipakai agar butir dari deskripsi elemen tidak mengulang materi yang sudah
    tertulis pada butir hasil pecahan Capaian Pembelajaran.
    """
    a = _kunci(x)
    if not a:
        return True
    for y in daftar:
        b = _kunci(y)
        if not b:
            continue
        if a[:20] and a[:20] in b:
            return True
        if b[:20] and b[:20] in a:
            return True
    return False


def _rapat_pokok(pokok, batas, pendek=24):
    """Rapatkan daftar pokok materi menjadi paling banyak `batas` butir.

    Pecahan yang sangat pendek disatukan dengan butir sebelumnya, lalu bila
    masih terlalu banyak, pasangan berdampingan yang paling pendek digabung
    lebih dulu supaya butir yang panjang dan bermakna tetap berdiri sendiri.
    """
    out = []
    for p in pokok:
        if out and len(p) < pendek:
            out[-1] = out[-1].rstrip(" .;,") + ", " + p
        else:
            out.append(p)
    while len(out) > max(1, batas) and len(out) > 1:
        i = min(range(len(out) - 1),
                key=lambda k: len(out[k]) + len(out[k + 1]))
        out[i] = out[i].rstrip(" .;,") + ", " + out[i + 1]
        del out[i + 1]
    return out


def pecah_tp_rinci(cp, elemen, deskripsi="", target=None,
                   minimum=MIN_TP, maksimum=MAKS_TP, sasaran=SASARAN_TP):
    """Menurunkan Tujuan Pembelajaran dari satu Capaian Pembelajaran.

    Jumlah butir TIDAK bergantung pada jumlah pertemuan: satu pertemuan boleh
    memuat beberapa TP, dan satu TP boleh diuraikan dalam beberapa pertemuan.
    Sasaran 6 butir, dibatasi 4 sampai 8 butir.

    Sumbernya berjenjang, dari yang paling sahih lebih dulu:
      1. perincian di dalam naskah Capaian Pembelajaran,
      2. deskripsi elemen pada tabel Karakteristik dokumen CP,
      3. tahap kognitif bertingkat atas pokok materi yang sama.
    Lapis 3 tidak menambah materi baru, hanya menaikkan tuntutan berpikir.
    """
    nm = re.sub(r"\s*\([^)]*\)", "", elemen or "").strip()
    bersih = _tanpa_terjemahan(cp)
    kerja = _kerja_awal(bersih) or "menjelaskan"

    minta = int(target or 0)
    sasaran = max(minimum, min(maksimum, minta if minta else sasaran))

    # -- lapis 1: perincian di dalam Capaian Pembelajaran ------------------
    pokok = _pokok_cp(bersih)
    if not pokok:
        pokok = [_inti_frasa(bersih, 150) or nm.lower()]
    pokok = _rapat_pokok(pokok, sasaran if len(pokok) > sasaran else len(pokok))

    # -- lapis 2: deskripsi elemen ----------------------------------------
    if len(pokok) < sasaran and deskripsi and len(deskripsi) > 40:
        for x in _pokok_cp(deskripsi):
            if len(pokok) >= sasaran:
                break
            if len(x) >= 16 and not _tumpang(x, pokok):
                pokok.append(x)

    # -- rumusan butir ----------------------------------------------------
    tp = ["%s %s" % (kerja, p) for p in pokok]

    # -- lapis 3: tahap kognitif bertingkat -------------------------------
    i = 0
    while len(tp) < max(minimum, min(sasaran, maksimum)):
        p = _inti_frasa(pokok[i % len(pokok)]) if pokok else nm.lower()
        tp.append("%s %s" % (TAHAP_TP[i % len(TAHAP_TP)], p))
        i += 1
        if i > 20:
            break

    tp = tp[:maksimum]
    return ["%d. Murid mampu %s." % (n + 1, kecil_awal(_potong_tp(x)))
            for n, x in enumerate(tp)]


def buat_materi(cp, deskripsi, elemen):
    sumber = deskripsi if deskripsi and len(deskripsi) > 40 else cp
    kal = re.split(r"\.\s+|;\s*|,\s+(?=dan |serta )", re.sub(r"\s+", " ", sumber))
    poin = []
    for k in kal:
        k = k.strip(" .;")
        if len(k) < 12:
            continue
        k = re.sub(r"^(Meliputi|Proses|Menekankan pada|Dilakukan melalui tahapan|Melalui)\s+",
                   "", k, flags=re.I)
        if len(k) > 130:
            k = k[:127].rsplit(" ", 1)[0] + "\u2026"
        poin.append("\u2022 " + kapital(k))
        if len(poin) >= 5:
            break
    if not poin:
        poin = ["\u2022 " + elemen.split("(")[0].strip()]
    return "\n".join(poin)


# ============================================================================
# 6. PEMBAGIAN ELEMEN & MINGGU
# ============================================================================


def bagi_elemen(n, k):
    """Bagi n elemen ke k semester. Sisa diberikan ke semester terdepan (ganjil)."""
    if k <= 0:
        return []
    dasar = n // k
    sisa = n % k
    return [dasar + (1 if i < sisa else 0) for i in range(k)]


def bagi_minggu(bobot, total):
    """Bagi `total` minggu efektif proporsional terhadap bobot; jumlah dijamin tepat."""
    n = len(bobot)
    if n == 0:
        return []
    if n > total:
        raise ValueError("Jumlah elemen (%d) melebihi jumlah minggu efektif (%d)." % (n, total))
    tot_b = float(sum(bobot)) or 1.0
    mentah = [max(1.0, b / tot_b * total) for b in bobot]
    s = sum(mentah)
    mentah = [m / s * total for m in mentah]
    hasil = [max(1, int(math.floor(m))) for m in mentah]
    sisa = total - sum(hasil)
    frac = sorted(range(n), key=lambda i: -(mentah[i] - math.floor(mentah[i])))
    i = 0
    while sisa > 0:
        hasil[frac[i % n]] += 1
        sisa -= 1
        i += 1
    while sisa < 0:
        for i in sorted(range(n), key=lambda x: -hasil[x]):
            if hasil[i] > 1:
                hasil[i] -= 1
                sisa += 1
                break
        else:
            break
    return hasil


# ============================================================================
# 7. PENULISAN DOKUMEN
# ============================================================================


def style_run(r, size=10, bold=False):
    r.font.name = FONT
    r.font.size = Pt(size)
    r.bold = bold
    r._element.rPr.rFonts.set(qn("w:eastAsia"), FONT)


def set_cell(cell, text, align=WD_ALIGN_PARAGRAPH.JUSTIFY, bold=False, size=9):
    for p in list(cell.paragraphs):
        p._element.getparent().remove(p._element)
    for ln in (text.split("\n") if text else [""]):
        p = cell.add_paragraph()
        p.alignment = align
        p.paragraph_format.space_after = Pt(0)
        p.paragraph_format.space_before = Pt(0)
        style_run(p.add_run(ln), size, bold)


def shade(cell, color):
    tcPr = cell._tc.get_or_add_tcPr()
    shd = OxmlElement("w:shd")
    shd.set(qn("w:val"), "clear")
    shd.set(qn("w:fill"), color)
    tcPr.append(shd)


HEADERS = ["Elemen", "Capaian Pembelajaran", "Alur Tujuan Pembelajaran (ATP)",
           "Tujuan Pembelajaran", "8 Dimensi Profil Lulusan", "Materi Pokok",
           "Media Pembelajaran", "Alokasi Waktu"]
WIDTHS = [3.0, 4.5, 5.5, 5.5, 3.2, 4.3, 3.5, 2.9]


def tulis_docx(meta, blok, out_path):
    doc = Document()
    sec = doc.sections[0]
    sec.orientation = WD_ORIENT.LANDSCAPE
    sec.page_width, sec.page_height = Cm(33.0), Cm(21.5)
    sec.left_margin = sec.right_margin = Cm(1.5)
    sec.top_margin = sec.bottom_margin = Cm(1.5)
    doc.styles["Normal"].font.name = FONT
    doc.styles["Normal"].font.size = Pt(10)

    def para(t, bold=False, size=11, align=WD_ALIGN_PARAGRAPH.LEFT):
        p = doc.add_paragraph()
        p.alignment = align
        p.paragraph_format.space_after = Pt(0)
        style_run(p.add_run(t), size, bold)

    para("ALUR TUJUAN PEMBELAJARAN KURIKULUM NASIONAL", True, 12, WD_ALIGN_PARAGRAPH.CENTER)
    para("FASE %s \u2013 KELAS %s \u2013 %s" % (meta["fase"], meta["kelas"], meta["jenjang"]),
         True, 12, WD_ALIGN_PARAGRAPH.CENTER)
    para("")
    para("Satuan Pendidikan\t: %s" % meta["sekolah"])
    para("Mata Pelajaran\t\t: %s" % meta["mapel"])
    para("Kelas / Fase\t\t: %s / %s" % (meta["kelas"], meta["fase"]))
    para("Tahun Pelajaran\t\t: %s" % meta["tahun"])
    para("Penyusun\t\t: %s  (%s)"
         % ((meta.get("penyusun") or "").strip() or TITIK, npa_teks(meta.get("npa"))))
    para("Alokasi Waktu\t\t: %d JP per minggu (1 JP = %d menit)" % (meta["jp"], meta["menit"]))
    para("Pola Pertemuan\t\t: %d pertemuan per minggu x %d JP "
         "(%d menit per pertemuan)"
         % (meta["tm_minggu"], meta["jpp"], meta["jpp"] * meta["menit"]))
    para("Minggu Efektif\t\t: %s" % meta["info_minggu"])
    para("Rujukan CP\t\t: Kepkabadan Kemendikdasmen No. 046/H/KR/2025, Lampiran %s (%s)"
         % (meta["lampiran"], meta["kode"]))
    para("")

    total_rows = 1 + sum(1 + len(b["baris"]) for b in blok)
    t = doc.add_table(rows=total_rows, cols=8)
    t.style = "Table Grid"
    t.alignment = WD_TABLE_ALIGNMENT.CENTER

    for j, h in enumerate(HEADERS):
        c = t.rows[0].cells[j]
        set_cell(c, h, WD_ALIGN_PARAGRAPH.CENTER, True, 9)
        shade(c, "D9E2F3")

    ri = 1
    for b in blok:
        row = t.rows[ri]
        m = row.cells[0]
        for j in range(1, 8):
            m = m.merge(row.cells[j])
        set_cell(m, b["judul"], WD_ALIGN_PARAGRAPH.CENTER, True, 10)
        shade(m, "FFF2CC" if b["aktif"] else "E7E6E6")
        ri += 1
        for data in b["baris"]:
            r = t.rows[ri]
            for j, val in enumerate(data):
                al = WD_ALIGN_PARAGRAPH.CENTER if j in (0, 7) else WD_ALIGN_PARAGRAPH.JUSTIFY
                set_cell(r.cells[j], val, al, size=9)
            ri += 1

    t.autofit = False
    for r in t.rows:
        for j, w in enumerate(WIDTHS):
            if j < len(r.cells):
                r.cells[j].width = Cm(w)

    doc.add_paragraph("")
    para("Rekapitulasi Alokasi Waktu:", True, 10)
    for b in blok:
        if b["aktif"]:
            para("%s : %d minggu efektif x %d JP = %d JP  |  %d pertemuan x %d JP  |  %d menit"
                 % (b["nama"], b["minggu"], meta["jp"], b["jp"],
                    b["tm"], meta["jpp"], b["jp"] * meta["menit"]), size=10)
        else:
            para("%s : %s" % (b["nama"], b["catatan"]), size=10)
    tm = sum(b["minggu"] for b in blok if b["aktif"])
    tj = sum(b["jp"] for b in blok if b["aktif"])
    tt = sum(b["tm"] for b in blok if b["aktif"])
    para("Total Kelas %s : %d minggu efektif = %d pertemuan = %d JP = %d menit"
         % (meta["kelas"], tm, tt, tj, tj * meta["menit"]), True, 10)
    para("")
    para("Catatan: Rumusan Elemen dan Capaian Pembelajaran disalin sesuai dokumen rujukan resmi. "
         "ATP, Tujuan Pembelajaran, Materi Pokok, dan Media Pembelajaran merupakan penjabaran "
         "operasional yang dapat disesuaikan dengan kondisi satuan pendidikan.", size=9)

    para("")
    para("")
    blok_ttd(doc, meta.get("kota", "Jambi"), meta.get("penyusun"),
             meta.get("npa"), meta["sekolah"], lebar=(15.0, 15.0))

    doc.save(out_path)
    return out_path


# ============================================================================
# 8. MESIN UTAMA
# ============================================================================


# ============================================================================
# IDENTITAS TETAP SATUAN PENDIDIKAN
# ============================================================================
# Nama dan NPA Kepala Sekolah bersifat tetap untuk seluruh dokumen (ATP, RPM,
# KKTP), jadi tidak ditanyakan lagi pada form. Nama dan NPA Guru Mata Pelajaran
# selalu diisi melalui form.

KEPALA_NAMA = "YAKUB ISHAK, S.E."
KEPALA_NPA = "NPA. 05011100009"
SEKOLAH_TETAP = "SMKS PGRI 2 Jambi"
TITIK = "..........................."
BULAN_ID = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli",
            "Agustus", "September", "Oktober", "November", "Desember"]


def npa_teks(npa):
    """Rapikan penulisan NPA guru; kosong menjadi titik-titik untuk diisi tangan."""
    x = re.sub(r"\s+", " ", (npa or "")).strip()
    if not x or set(x) <= set(". "):
        return "NPA. ............................."
    if not x.upper().startswith("NPA"):
        x = "NPA. " + x
    return x


def tanggal_ttd(kota="Jambi"):
    """Tanggal pada kolom tanda tangan, mis. "Jambi, Juli 2026"."""
    import datetime
    n = datetime.date.today()
    return "%s, %s %d" % (kota, BULAN_ID[n.month - 1], n.year)


def blok_ttd(doc, kota="Jambi", penyusun=None, npa=None, sekolah=None,
             lebar=(8.5, 8.5), ukuran=10):
    """Tabel tanda tangan dua kolom: Kepala Sekolah (tetap) dan Guru Mata Pelajaran.

    Dipakai bersama oleh ATP, RPM, dan KKTP agar bentuknya seragam.
    """
    kiri = ["Mengetahui,", "Kepala %s" % (sekolah or SEKOLAH_TETAP), "", "",
            KEPALA_NAMA, KEPALA_NPA]
    kanan = [tanggal_ttd(kota), "Guru Mata Pelajaran,", "", "",
             (penyusun or "").strip() or TITIK, npa_teks(npa)]
    t = doc.add_table(rows=len(kiri), cols=2)
    t.alignment = WD_TABLE_ALIGNMENT.CENTER
    for i in range(len(kiri)):
        for j, v in enumerate((kiri[i], kanan[i])):
            set_cell(t.rows[i].cells[j], v, WD_ALIGN_PARAGRAPH.CENTER,
                     i == 4, ukuran)
    t.autofit = False
    for r in t.rows:
        for j, w in enumerate(lebar):
            if j < len(r.cells):
                r.cells[j].width = Cm(w)
    return t


def pilih_dpl_list(*teks, **kw):
    """Versi pilih_dpl yang mengembalikan daftar, bukan teks bernomor titik."""
    minimal = kw.get("minimal", 3)
    maksimal = kw.get("maksimal", 4)
    blob = " ".join(t.lower() for t in teks if t)
    skor = []
    for nama, keys in DPL:
        skor.append((sum(blob.count(k) for k in keys), nama))
    skor.sort(key=lambda x: -x[0])
    dipilih = [n for s, n in skor if s > 0][:maksimal]
    if len(dipilih) < minimal:
        for s, n in skor:
            if n not in dipilih:
                dipilih.append(n)
            if len(dipilih) >= minimal:
                break
    urut = [n for n, _ in DPL]
    dipilih.sort(key=urut.index)
    return dipilih


def _konteks_elemen(nama):
    """Ambil inti nama elemen untuk dipakai sebagai pembeda Tujuan Pembelajaran."""
    x = re.sub(r"\(.*?\)", " ", nama or "")
    x = re.sub(r"\s+", " ", x).strip(" .")
    x = re.sub(r"^(perawatan dan perbaikan|perawatan|perbaikan|dasar-dasar|dasar)\s+",
               "", x, flags=re.IGNORECASE)
    return kecil_awal(x)[:60].strip(" .")


def _bedakan_tp(butir, nama_elemen, dipakai):
    """Beri konteks elemen pada butir TP yang rumusannya sudah pernah dipakai."""
    konteks = _konteks_elemen(nama_elemen)
    hasil = []
    for b in butir:
        m = re.match(r"^(\d+)\.\s*(.*)$", b.strip())
        no, isi = (m.group(1), m.group(2)) if m else ("", b.strip())
        kunci = re.sub(r"[^a-z0-9 ]", "", isi.lower()).strip()
        if kunci and kunci in dipakai and konteks:
            isi = isi.rstrip(". ") + " pada " + konteks + "."
            kunci = re.sub(r"[^a-z0-9 ]", "", isi.lower()).strip()
        dipakai.add(kunci)
        hasil.append(("%s. %s" % (no, isi)) if no else isi)
    return hasil


def susun_rencana(mapel, jp, kelas=None, fase=None, mg=22, mn=15, menit=45, jpp=None,
                  sekolah="SMKS PGRI 2 Jambi", tahun="2026/2027", elemen_kelas=None,
                  penyusun=None, npa=None, kota="Jambi"):
    """Menghitung seluruh rencana alokasi ATP.

    Dipakai bersama oleh generator ATP dan generator RPM supaya kedua dokumen
    selalu memakai angka yang sama persis (sinkron).
    """
    # --- pola pertemuan ---------------------------------------------------
    jpp = jpp or jp
    if jpp <= 0 or jp <= 0:
        print("\u274c JP per minggu dan JP per pertemuan harus lebih dari 0.")
        sys.exit(1)
    if jp % jpp != 0:
        print("\u274c %d JP per minggu tidak habis dibagi %d JP per pertemuan." % (jp, jpp))
        print("   Pilihan yang pas untuk %d JP/minggu: %s"
              % (jp, ", ".join(str(d) for d in range(1, jp + 1) if jp % d == 0)))
        sys.exit(1)
    tm_minggu = jp // jpp

    db = load_db()

    # --- tentukan fase & kelas -------------------------------------------
    if kelas:
        kelas = kelas.upper().strip()
        if kelas not in KELAS_FASE:
            print("\u274c Kelas harus X, XI, atau XII.")
            sys.exit(1)
        fase = fase or KELAS_FASE[kelas]
    else:
        fase = (fase or "E").upper()
        kelas = "X" if fase == "E" else "XI"
    if (fase, kelas) not in STRUKTUR:
        print("\u274c Kombinasi Fase %s / Kelas %s tidak berlaku untuk SMK." % (fase, kelas))
        print("   Yang berlaku: E/X, F/XI, F/XII")
        sys.exit(1)

    # --- cari mapel -------------------------------------------------------
    kandidat = cari_mapel(db, mapel, fase)
    if not kandidat:
        print("\u274c Mata pelajaran '%s' tidak ditemukan untuk Fase %s." % (mapel, fase))
        for s in cari_mapel(db, mapel.split()[0] if mapel.split() else mapel)[:8]:
            print("   - %s  [Lampiran %s %s, fase: %s]"
                  % (s["nama_asli"].title(), s["lampiran"], s["kode"], ", ".join(sorted(s["fase"]))))
        sys.exit(1)
    s = kandidat[0]
    elemen_all = s["fase"][fase]
    N = len(elemen_all)
    desc = {k["elemen"].lower(): k["deskripsi"] for k in s.get("karakteristik", [])}

    # --- porsi elemen untuk kelas ini (khusus Fase F yang menjangkau XI-XII)
    struktur = STRUKTUR[(fase, kelas)]
    n_aktif_kelas_ini = len(semester_aktif(fase, kelas))

    if fase == "F":
        n_xi = len(semester_aktif("F", "XI"))     # 2 semester
        n_xii = len(semester_aktif("F", "XII"))   # 1 semester (sem 5 = PKL)
        total_aktif = n_xi + n_xii                # 3 semester efektif
        if elemen_kelas is None:
            porsi_xi = int(math.ceil(N * float(n_xi) / total_aktif))
            porsi_xi = max(1, min(N - 1, porsi_xi)) if N > 1 else 1
        else:
            porsi_xi = max(0, min(N, elemen_kelas))
        if kelas == "XI":
            elemen = elemen_all[:porsi_xi]
            offset = 0
        else:
            elemen = elemen_all[porsi_xi:]
            offset = porsi_xi
    else:
        elemen = elemen_all
        offset = 0
        porsi_xi = None

    if not elemen:
        print("\u274c Tidak ada elemen tersisa untuk Kelas %s." % kelas)
        sys.exit(1)

    # --- bagi elemen ke semester aktif (sisa ke ganjil) -------------------
    pembagian = bagi_elemen(len(elemen), n_aktif_kelas_ini)

    # --- susun blok -------------------------------------------------------
    blok = []
    idx = 0
    ptr = 0
    peringatan = []
    dipakai_tp = set()
    for (no, label, jenis, aktif, catatan) in struktur:
        if not aktif:
            blok.append({"nama": label, "judul": "%s \u2013 %s" % (label, catatan),
                         "baris": [], "detail": [], "minggu": 0, "jp": 0, "tm": 0,
                         "aktif": False, "no": no, "jenis": jenis, "catatan": catatan})
            continue
        minggu = mg if jenis == "ganjil" else mn
        lo, hi = WAJAR_GANJIL if jenis == "ganjil" else WAJAR_GENAP
        if not (lo <= minggu <= hi):
            peringatan.append("%s: %d minggu (kelaziman %d\u2013%d minggu)" % (label, minggu, lo, hi))
        jumlah = pembagian[idx]
        idx += 1
        els = elemen[ptr:ptr + jumlah]
        mulai = ptr
        ptr += jumlah
        bobot = [len(e["cp"]) for e in els]
        bagi = bagi_minggu(bobot, minggu)
        baris = []
        detail = []
        for j, (e, wk) in enumerate(zip(els, bagi)):
            nm = e["elemen"]
            d = ""
            for k, v in desc.items():
                key = k.split("(")[0].strip()[:25]
                if key and key in nm.lower():
                    d = v
                    break
            jp_el = wk * jp
            teks_atp = buat_atp(e["cp"], nm)
            # Kolom Tujuan Pembelajaran pada ATP memakai daftar yang sama
            # persis dengan yang dipakai RPM dan KKTP.
            tp_rinci = pecah_tp_rinci(e["cp"], nm, d)
            tp_rinci = _bedakan_tp(tp_rinci, nm, dipakai_tp)
            teks_tp = "\n".join(tp_rinci)
            teks_dpl = pilih_dpl(nm, e["cp"], d, s.get("rasional", "")[:400])
            teks_materi = buat_materi(e["cp"], d, nm)
            teks_media = pilih_media(s["nama_asli"], nm)
            alokasi = ("%d JP\n%d minggu x %d JP\n%d pertemuan x %d JP\n= %d menit"
                       % (jp_el, wk, jp, wk * tm_minggu, jpp, jp_el * menit))
            baris.append([
                nm.replace(" (", "\n("),
                e["cp"],
                teks_atp,
                teks_tp,
                teks_dpl,
                teks_materi,
                teks_media,
                alokasi,
            ])
            tm_el = wk * tm_minggu
            detail.append({
                "tp_list": tp_rinci,
                "urut": offset + mulai + j + 1,
                "urut_kelas": mulai + j + 1,
                "elemen": nm,
                "cp": e["cp"],
                "deskripsi": d,
                "atp": teks_atp,
                "tp": teks_tp,
                "dpl": pilih_dpl_list(nm, e["cp"], d, s.get("rasional", "")[:400]),
                "materi": teks_materi,
                "media": teks_media,
                "minggu": wk,
                "jp": jp_el,
                "tm": wk * tm_minggu,
                "alokasi": alokasi,
                "semester": no,
                "label_semester": label,
                "jenis": jenis,
            })
        blok.append({"nama": label,
                     "judul": "%s \u2013 %d MINGGU EFEKTIF / %d PERTEMUAN / %d JP"
                              % (label, minggu, minggu * tm_minggu, minggu * jp),
                     "baris": baris, "detail": detail,
                     "minggu": minggu, "jp": minggu * jp,
                     "tm": minggu * tm_minggu,
                     "aktif": True, "no": no, "jenis": jenis, "catatan": ""})

    info_minggu = "Ganjil %d minggu, Genap %d minggu" % (mg, mn) if n_aktif_kelas_ini == 2 \
        else "Genap %d minggu (Semester 5 = PKL)" % mn

    nm_mapel = s["nama_asli"].title()
    meta = {"mapel": nm_mapel, "fase": fase, "kelas": kelas, "jp": jp, "menit": menit,
            "jpp": jpp, "tm_minggu": tm_minggu,
            "sekolah": sekolah, "tahun": tahun, "lampiran": s["lampiran"], "kode": s["kode"],
            "penyusun": penyusun, "npa": npa, "kota": kota,
            "kepala": KEPALA_NAMA, "kepala_npa": KEPALA_NPA,
            "info_minggu": info_minggu, "mg": mg, "mn": mn,
            "jenjang": "SMK/MAK (SEMUA PROGRAM KEAHLIAN)" if s["lampiran"] == "II" else "SMK/MAK"}

    return {"s": s, "meta": meta, "blok": blok, "peringatan": peringatan,
            "elemen_all": elemen_all, "elemen": elemen, "offset": offset,
            "porsi_xi": porsi_xi, "N": N, "desc": desc,
            "n_semester_aktif": n_aktif_kelas_ini}


def generate(mapel, jp, kelas=None, fase=None, mg=22, mn=15, menit=45, jpp=None,
             sekolah="SMKS PGRI 2 Jambi", tahun="2026/2027", out=None,
             penyusun=None, npa=None, kota="Jambi",
             elemen_kelas=None, diam=False):
    r = susun_rencana(mapel, jp, kelas=kelas, fase=fase, mg=mg, mn=mn, menit=menit,
                      jpp=jpp, sekolah=sekolah, tahun=tahun, elemen_kelas=elemen_kelas,
                      penyusun=penyusun, npa=npa, kota=kota)
    s = r["s"]
    meta = r["meta"]
    blok = r["blok"]
    fase = meta["fase"]
    kelas = meta["kelas"]
    jp = meta["jp"]
    jpp = meta["jpp"]
    menit = meta["menit"]
    tm_minggu = meta["tm_minggu"]
    nm_mapel = meta["mapel"]
    N = r["N"]
    porsi_xi = r["porsi_xi"]
    offset = r["offset"]
    elemen = r["elemen"]

    if out is None:
        aman = re.sub(r"[^A-Za-z0-9 ]", "", nm_mapel).strip().upper()
        out = os.path.join(BASE_OUT, "ATP %s - KELAS %s FASE %s.docx" % (aman, kelas, fase))
    out = os.path.abspath(out)
    tulis_docx(meta, blok, out)

    if not diam:
        print("\u2705 BERHASIL")
        print("   Mapel        : %s  (Lampiran %s, %s)" % (nm_mapel, s["lampiran"], s["kode"]))
        print("   Fase / Kelas : %s / %s" % (fase, kelas))
        print("   Elemen fase  : %d elemen" % N)
        if fase == "F":
            print("   Porsi kelas  : XI = elemen 1-%d  |  XII = elemen %d-%d"
                  % (porsi_xi, porsi_xi + 1, N))
            print("   Dipakai kini : %d elemen (elemen %d-%d)"
                  % (len(elemen), offset + 1, offset + len(elemen)))
        for b in blok:
            if b["aktif"]:
                print("   %-20s %2d mgg x %2d JP = %4d JP  | %3d pertemuan x %d JP  | %d elemen"
                      % (b["nama"], b["minggu"], jp, b["jp"], b["tm"], jpp, len(b["baris"])))
            else:
                print("   %-20s %s" % (b["nama"], b["catatan"]))
        for p in r["peringatan"]:
            print("   \u26a0\ufe0f  %s" % p)
        print("   Pola         : %d JP/minggu = %d pertemuan x %d JP (%d menit/pertemuan)"
              % (jp, tm_minggu, jpp, jpp * menit))
        print("")
        print("   \U0001f4c1 TERSIMPAN DI:")
        print("   %s" % out)
    return out


# ============================================================================
# 9. MODE FORM INTERAKTIF
# ============================================================================


def mode_form():
    db = load_db()
    print("=" * 70)
    print("           FORM GENERATOR ATP \u2013 KURIKULUM NASIONAL")
    print("      Rujukan: Kepkabadan Kemendikdasmen No. 046/H/KR/2025")
    print("=" * 70)

    while True:
        q = input("\n[1] Mata Pelajaran        : ").strip()
        hasil = cari_mapel(db, q)
        if not hasil:
            print("    \u274c Tidak ditemukan. Coba kata kunci lain.")
            continue
        if len(hasil) > 1:
            print("    Ditemukan beberapa:")
            for i, s in enumerate(hasil[:10], 1):
                print("      %2d) %-55s [Lamp %s %s, fase %s]"
                      % (i, s["nama_asli"].title()[:55], s["lampiran"], s["kode"],
                         ", ".join(sorted(s["fase"]))))
            p = input("    Pilih nomor [1]: ").strip() or "1"
            try:
                s = hasil[int(p) - 1]
            except Exception:
                s = hasil[0]
        else:
            s = hasil[0]
        break

    print("    \u2192 %s (fase tersedia: %s)" % (s["nama_asli"].title(), ", ".join(sorted(s["fase"]))))
    print("\n[2] Fase / Kelas")
    print("      1) Fase E / Kelas X    \u2192 Semester 1 (Ganjil) + Semester 2 (Genap)")
    print("      2) Fase F / Kelas XI   \u2192 Semester 3 (Ganjil) + Semester 4 (Genap)")
    print("      3) Fase F / Kelas XII  \u2192 Semester 6 (Genap)  [Sem 5 = PKL]")
    pk = (input("    Pilih [1/2/3]: ").strip() or "1")
    kelas = {"1": "X", "2": "XI", "3": "XII"}.get(pk, "X")

    while True:
        try:
            jp = int(input("\n[3] JP per minggu         : ").strip())
            if jp > 0:
                break
        except ValueError:
            pass
        print("    \u274c Isi dengan angka lebih dari 0.")

    faktor = [d for d in range(1, jp + 1) if jp % d == 0]
    print("\n[4] JP dalam 1x pertemuan  (pilihan: %s)" % ", ".join(map(str, faktor)))
    while True:
        d = input("      - JP per pertemuan [%d] : " % jp).strip()
        try:
            jpp = int(d) if d else jp
        except ValueError:
            jpp = -1
        if jpp in faktor:
            break
        print("    \u274c %d JP/minggu tidak habis dibagi. Pilih: %s"
              % (jp, ", ".join(map(str, faktor))))
    print("    \u2192 %d pertemuan per minggu x %d JP = %d JP/minggu"
          % (jp // jpp, jpp, jp))

    print("\n[5] Jumlah Minggu Efektif")
    if kelas == "XII":
        mg = 0
        mn = int(input("      - Semester Genap   [ ] : ").strip())
    else:
        mg = int(input("      - Semester Ganjil  [ ] : ").strip())
        mn = int(input("      - Semester Genap   [ ] : ").strip())

    sekolah = input("\n[6] Satuan Pendidikan [SMKS PGRI 2 Jambi] : ").strip() or "SMKS PGRI 2 Jambi"
    tahun = input("[7] Tahun Pelajaran   [2026/2027]        : ").strip() or "2026/2027"

    print("\n[8] Guru Mata Pelajaran (untuk identitas dan kolom tanda tangan)")
    print("      Kepala Sekolah sudah tetap: %s (%s)" % (KEPALA_NAMA, KEPALA_NPA))
    penyusun = input("      - Nama/Penyusun    : ").strip()
    npa = input("      - NPA              : ").strip()
    kota = input("      - Kota [Jambi]     : ").strip() or "Jambi"

    print("\n" + "-" * 70)
    generate(s["nama_asli"], jp, kelas=kelas, mg=mg or 22, mn=mn, jpp=jpp,
             sekolah=sekolah, tahun=tahun, penyusun=penyusun, npa=npa, kota=kota)


# ============================================================================
# 10. CLI
# ============================================================================


def main():
    ap = argparse.ArgumentParser(description="Generator ATP dari CP resmi No. 046/H/KR/2025")
    ap.add_argument("--mapel")
    ap.add_argument("--jp", type=int, help="JP per minggu")
    ap.add_argument("--jpp", "--jp-pertemuan", dest="jpp", type=int, default=None,
                    help="JP dalam 1x pertemuan (bawaan: sama dengan --jp)")
    ap.add_argument("--kelas", default=None, help="X | XI | XII")
    ap.add_argument("--fase", choices=list("ABCDEF"), default=None)
    ap.add_argument("--mg", "--minggu-ganjil", dest="mg", type=int, default=22,
                    help="Minggu efektif semester ganjil")
    ap.add_argument("--mn", "--minggu-genap", dest="mn", type=int, default=15,
                    help="Minggu efektif semester genap")
    ap.add_argument("--menit", type=int, default=45)
    ap.add_argument("--sekolah", default="SMKS PGRI 2 Jambi")
    ap.add_argument("--tahun", default="2026/2027")
    ap.add_argument("--penyusun", default=None, help="nama Guru Mata Pelajaran")
    ap.add_argument("--npa", default=None, help="NPA Guru Mata Pelajaran")
    ap.add_argument("--kota", default="Jambi")
    ap.add_argument("--elemen-kelas11", dest="elemen_kelas", type=int, default=None,
                    help="Jumlah elemen Fase F yang dialokasikan ke Kelas XI")
    ap.add_argument("--out", default=None)
    ap.add_argument("--list", dest="cari", default=None)
    ap.add_argument("--form", action="store_true", help="Mode form interaktif")
    a = ap.parse_args()

    if a.form:
        mode_form()
        return

    if a.cari is not None:
        db = load_db()
        hasil = cari_mapel(db, a.cari, a.fase)
        if not hasil:
            print("Tidak ada mapel yang cocok dengan '%s'." % a.cari)
            return
        print("Ditemukan %d mata pelajaran:\n" % len(hasil))
        print("%-6s %-9s %-58s %s" % ("LAMP", "KODE", "MATA PELAJARAN", "FASE"))
        print("-" * 92)
        for s in hasil[:40]:
            print("%-6s %-9s %-58s %s" % (s["lampiran"], s["kode"], s["nama_asli"].title()[:58],
                                          ", ".join(sorted(s["fase"]))))
        return

    if not a.mapel or not a.jp:
        ap.error("--mapel dan --jp wajib diisi (atau gunakan --form / --list).")

    generate(a.mapel, a.jp, kelas=a.kelas, fase=a.fase, mg=a.mg, mn=a.mn, menit=a.menit,
             jpp=a.jpp, sekolah=a.sekolah, tahun=a.tahun, out=a.out,
             penyusun=a.penyusun, npa=a.npa, kota=a.kota,
             elemen_kelas=a.elemen_kelas)


if __name__ == "__main__":
    main()
